﻿using System;
using System.Net;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

namespace PickGold.Base.Proxies
{
	/// <summary>
	/// Http Proxy
	/// </summary>
	public abstract class HttpProxy : IDisposable
	{
		#region internal class declare

		/// <summary>
		/// Proxy Context
		/// </summary>
		protected class ProxyContext : IDisposable
		{
			public Stream Output;
			public MemoryStream Input;
			public HttpListenerContext Context;
			public HttpWebRequest Request;
			public HttpWebResponse Response;

			public virtual void Dispose()
			{
				if (this.Context != null)
				{
					if (this.Context.Response != null)
						this.Context.Response.Close();
				}
				if (this.Response != null)
					this.Response.Close();
				this.Output = null;
				this.Input = null;
				this.Context = null;
				this.Request = null;
				this.Response = null;
			}
		}

		/// <summary>
		/// Async Result
		/// </summary>
		protected class AsyncResult : IAsyncResult
		{
			#region IAsyncResult 成员

			public object AsyncState { set; get; }

			public WaitHandle AsyncWaitHandle { set; get; }

			public bool CompletedSynchronously { set; get; }

			public bool IsCompleted { set; get; }

			#endregion
		}

		#endregion

		protected int _Paused = 0;
		/// <summary>
		/// 代理信息 Header 【HTTP_VIA】 值
		/// </summary>
		protected string _HeaderValue = null;
		protected Exception _LastError = null;
		protected HttpListener _Listener = null;

		public HttpProxy()
		{
			this._HeaderValue = this.GetType().Namespace + " " + this.GetType().Assembly.GetName().Version.ToString();
		}

		~HttpProxy()
		{
			this.Dispose(true);
		}

		public virtual bool Paused
		{
			set
			{
				if (value)
				{
					if (this._Paused >= 0)
						return;

					this._Paused = 0;
					this.BeginGetContext();
					return;
				}

				this._Paused = short.MinValue;
			}
			get
			{
				if (this._Paused >= 0)
					return false;

				return true;
			}
		}

		protected virtual ProxyContext CreateProxyContext()
		{
			return new ProxyContext();
		}

		public virtual void Start(string[] prefixes)
		{
			if (this._Listener == null)
				this._Listener = new HttpListener();
			if (!this._Listener.IsListening)
				this._Listener.Start();
			if (prefixes != null && prefixes.Length > 0)
			{
				foreach (var prefix in prefixes)
					this._Listener.Prefixes.Add(prefix);
			}
			this.Paused = true;
			this.BeginGetContext();
		}

		public virtual void Stop()
		{
			this._Paused = 0;
			this._LastError = null;
			if (this._Listener != null && this._Listener.IsListening)
				this._Listener.Stop();
			this._Listener = null;
		}

		protected abstract string GetRequestUrl(ProxyContext pc);

		protected abstract string GetReferer(ProxyContext pc);

		protected virtual CookieContainer GetCookieContainer(ProxyContext pc)
		{
			return null;
		}

		protected virtual void OnError(ProxyContext pc, Exception error)
		{
			this._LastError = error;
			if (pc == null || pc.Context == null)
				return;

			//var hle = error as HttpListenerException;
			//if (hle != null)
			//{
			//    if (hle.ErrorCode == 1229 || hle.ErrorCode == 64)
			//        return;
			//}
			if (error is HttpListenerException)
				return;

			try
			{
				pc.Context.Response.StatusCode = HttpStatus.Value.BAD_REQUEST;
				pc.Context.Response.StatusDescription = HttpStatus.Message.BAD_REQUEST;
				pc.Context.Response.Close();
			}
			catch (Exception x)
			{
				Tracker.WriteError(x);
				this._LastError = x;
			}
		}

		protected virtual void BeginGetContext()
		{
			if (this._Paused != 0)
				return;

			if (this._Listener == null)
				return;

			if (!this._Listener.IsListening)
				return;

			var pc = this.CreateProxyContext();
			if (pc == null)
				return;

			Interlocked.Increment(ref this._Paused);
			this._Listener.BeginGetContext(this.EndGetContext, pc);
		}

		protected virtual void EndGetContext(IAsyncResult asyncResult)
		{
			var pc = asyncResult.AsyncState as ProxyContext;
			Interlocked.Decrement(ref this._Paused);
			try
			{
				pc.Context = this._Listener.EndGetContext(asyncResult);
			}
			catch (Exception x)
			{
				Tracker.WriteError(x);
				this.OnError(pc, x);
				this.BeginGetContext();
				return;
			}

			this.BeginGetContext();
			try
			{
				this.EndGetContext(pc);
			}
			catch (Exception x)
			{
				Tracker.WriteError(x);
				this.OnError(pc, x);
			}
		}

		protected virtual void EndGetContext(ProxyContext pc)//x
		{
			var url = this.GetRequestUrl(pc);
			if (string.IsNullOrEmpty(url))
				return;

			//pc.Request = HttpWebRequest.Create("http://localhost:8000" + context.Request.RawUrl) as HttpWebRequest;
			pc.Request = WebRequest.Create(url) as HttpWebRequest;
			pc.Request.CookieContainer = this.GetCookieContainer(pc);
			if (pc.Request.CookieContainer != null)
			{
				var ccs = pc.Request.CookieContainer.GetCookies(pc.Context.Request.Url);
				for (var i = 0; i < pc.Context.Request.Cookies.Count; i++)
				{
					var cc = pc.Context.Request.Cookies[i];
					if (/*string.IsNullOrEmpty(cc.Domain) || string.IsNullOrEmpty(cc.Path) || */string.IsNullOrEmpty(cc.Name))
					{
						cc = null;
						continue;
					}

					for (var ii = 0; ii < ccs.Count; ii++)
					{
						if (//string.Compare(cc.Domain, ccs[ii].Domain, StringComparison.OrdinalIgnoreCase) == 0 &&
							//(string.IsNullOrEmpty(cc.Path) || ccs[ii].Path.StartsWith(cc.Path, StringComparison.OrdinalIgnoreCase) || string.Compare(cc.Path, ccs[ii].Path, StringComparison.OrdinalIgnoreCase) == 0) &&
							string.Compare(cc.Path, ccs[ii].Path, StringComparison.OrdinalIgnoreCase) == 0 &&
							string.Compare(cc.Name, ccs[ii].Name, StringComparison.OrdinalIgnoreCase) == 0 &&
							string.Compare(cc.Port, ccs[ii].Port, StringComparison.OrdinalIgnoreCase) == 0)
						{
							cc = null;
							continue;
						}
					}
					if (cc == null)
						continue;

					if (string.IsNullOrEmpty(cc.Domain))
						cc.Domain = pc.Request.Address.Host;
					pc.Request.CookieContainer.Add(cc);
				}
			}
			//HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
			this.SetRequestHeaders(pc);
			//if (string.Compare(pc.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) == 0)
			//    this.EndGetRequestStream(new AsyncResult { AsyncState = pc });
			//else
			//    pc.Request.BeginGetRequestStream(this.EndGetRequestStream, pc);
			if (pc.Context.Request.HasEntityBody)
				pc.Request.BeginGetRequestStream(this.EndGetRequestStream, pc);
			else
				this.EndGetRequestStream(new AsyncResult { AsyncState = pc });
		}

		protected virtual void SetRequestHeaders(ProxyContext pc)//./x
		{
			//pc.Request.Accept
			//pc.Request.Address
			//pc.Request.AllowAutoRedirect = false;
			pc.Request.AllowWriteStreamBuffering = false;
			//pc.Request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.None;
			//pc.Request.AutomaticDecompression = DecompressionMethods.None;
			//pc.Request.CachePolicy = new System.Net.Cache.RequestCachePolicy(RequestCacheLevel.Reload);
			//pc.Request.ClientCertificates = pc.Context.Request.BeginGetClientCertificate
			//pc.Request.ConnectionGroupName = context.Request.ContentEncoding
			//pc.Request.ContentLength = pc.Context.Request.ContentLength64;
			//pc.Request.ContentType = pc.Context.Request.ContentType;
			//pc.Request.ContinueDelegate = null;
			//pc.Request.CookieContainer = null;
			//pc.Request.Request.Credentials = pc.Context.Request.BeginGetClientCertificate
			//pc.Request.Expect = context.Request.ex
			//pc.Request.HaveResponse
			//pc.Request.Headers.Add(context.Request.Headers);
			//pc.Request.IfModifiedSince = DateTime.MinValue;
			//pc.Request.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Anonymous
			//pc.Request.KeepAlive
			//pc.Request.MaximumAutomaticRedirections
			//pc.Request.MaximumResponseHeadersLength
			//pc.Request.MediaType = context.Request.
			pc.Request.Method = pc.Context.Request.HttpMethod;
			//pc.Request.Pipelined
			//pc.Request.PreAuthenticate
			pc.Request.ProtocolVersion = pc.Context.Request.ProtocolVersion;
			//pc.Request.Proxy
			//pc.Request.ReadWriteTimeout
			//pc.Request.Referer
			//pc.Request.RequestUri
			//pc.Request.SendChunked = pc.Context.Request.se
			//pc.Request.ServicePoint.
			//pc.Request.Timeout
			//pc.Request.TransferEncoding
			//pc.Request.UnsafeAuthenticatedConnectionSharing
			//pc.Request.UseDefaultCredentials
			pc.Request.UserAgent = pc.Context.Request.UserAgent;
			foreach (string key in pc.Context.Request.Headers)
			{
				var value = pc.Context.Request.Headers[key];
				if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0)
				{
					if (string.Compare(value, "Keep-Alive", StringComparison.OrdinalIgnoreCase) == 0)
						pc.Request.KeepAlive = pc.Context.Request.KeepAlive;
					else if (string.Compare(value, "Close", StringComparison.OrdinalIgnoreCase) == 0)
						;//pc.Request.Connection = value;
					else
						pc.Request.Connection = value;
					continue;
				}

				if (string.Compare(key, "Accept", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.Accept = value;
				else if (string.Compare(key, "Host", StringComparison.OrdinalIgnoreCase) == 0)
					;//pc.Request.Accept = value;
				else if (string.Compare(key, "Range", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.AddRange(0);
				else if (string.Compare(key, "Referer", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.Referer = this.GetReferer(pc);
				else if (string.Compare(key, "Content-Type", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.ContentType = value;
				else if (string.Compare(key, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.ContentLength = long.Parse(value);
				else if (string.Compare(key, "If-Modified-Since", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Request.IfModifiedSince = DateTime.Parse(value);// context.Request
				else
					pc.Request.Headers[key] = value;
			}
			if (pc.Request.Headers["HTTP_VIA"] == null)
				pc.Request.Headers["HTTP_VIA"] = this._HeaderValue;
			else
				pc.Request.Headers["HTTP_VIA"] = pc.Request.Headers["HTTP_VIA"] + "; " + this._HeaderValue;
			if (pc.Request.Headers["HTTP_X_FORWARDED_FOR"] == null)
				pc.Request.Headers["HTTP_X_FORWARDED_FOR"] = pc.Context.Request.RemoteEndPoint.ToString();
			else
				pc.Request.Headers["HTTP_X_FORWARDED_FOR"] = pc.Request.Headers["HTTP_X_FORWARDED_FOR"] + "; " + pc.Context.Request.RemoteEndPoint;
		}

		protected virtual void EndGetRequestStream(IAsyncResult asyncResult)
		{
			var pc = asyncResult.AsyncState as ProxyContext;
			if (asyncResult is AsyncResult)
			{
				pc.Request.BeginGetResponse(this.EndGetResponse, pc);
				return;
			}

			try
			{
				var output = pc.Request.EndGetRequestStream(asyncResult);
				try
				{
					this.EndGetRequestStream(pc, output);
				}
				catch (Exception x)
				{
					Tracker.WriteError(x);
					this.OnError(pc, x);
				}
			}
			catch (Exception x)
			{
				Tracker.WriteError(x);
				//try
				//{
				//    this.EndGetRequestStream(pc, x);
				//}
				//catch (Exception xx)
				//{
				//    this.OnError(xx);
				//}
				this.OnError(pc, x);
			}
		}

		protected virtual void EndGetRequestStream(ProxyContext pc, Stream output)
		{
			if (pc.Input != null)
			{
				pc.Input.WriteTo(output);
				pc.Request.BeginGetResponse(this.EndGetResponse, pc);
				return;
			}

			var bs = new byte[BitUtil.K];
			var input = pc.Context.Request.InputStream;
			while (true)
			{
				var i = input.Read(bs, 0, bs.Length);
				if (i == 0)
					break;

				output.Write(bs, 0, i);
			}
			pc.Request.BeginGetResponse(this.EndGetResponse, pc);
		}

		protected virtual void EndGetRequestStream(ProxyContext pc, Exception x)//-
		{
			pc.Context.Response.StatusCode = HttpStatus.Value.BAD_REQUEST;
			pc.Context.Response.StatusDescription = HttpStatus.Message.BAD_REQUEST;
			pc.Context.Response.Close();
			this._LastError = x;
		}

		protected virtual void EndGetResponse(IAsyncResult asyncResult)
		{
			var pc = asyncResult.AsyncState as ProxyContext;
			try
			{
				pc.Response = pc.Request.EndGetResponse(asyncResult) as HttpWebResponse;
				try
				{
					this.EndGetResponse(pc);
				}
				catch (Exception x)
				{
					Tracker.WriteError(x);
					this.OnError(pc, x);
				}
			}
			catch (WebException x)
			{
				try
				{
					this.EndGetResponse(pc, x);
				}
				catch (Exception xx)
				{
					Tracker.WriteError(x);
					this.OnError(pc, xx);
				}
			}
			catch (HttpListenerException x)
			{
				Tracker.WriteError(x);
				//try
				//{
				//    this.EndGetResponse(pc, x);
				//}
				//catch (Exception xx)
				//{
				//    this.OnError(pc, xx);
				//}
				this.OnError(pc, x);
			}
			catch (Exception x)
			{
				Tracker.WriteError(x);
				//try
				//{
				//    this.EndGetResponse(pc, x);
				//}
				//catch (Exception xx)
				//{
				//    this.OnError(pc, xx);
				//}
				this.OnError(pc, x);
			}
		}

		protected virtual void EndGetResponse(ProxyContext pc)//x
		{
			pc.Context.Response.StatusCode = (int)pc.Response.StatusCode;
			pc.Context.Response.StatusDescription = pc.Response.StatusDescription;
			//pc.Context.Response.ContentType = pc.Response.ContentType;
			//pc.Context.Response.Headers.Add(pc.Response.Headers);
			foreach (string key in pc.Response.Headers.Keys)
			{
				var value = pc.Response.Headers[key];
				if (string.Compare(key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase) == 0)
				{
					if (string.Compare(value, "chunked", StringComparison.OrdinalIgnoreCase) == 0)
						pc.Context.Response.SendChunked = true;
					else
						pc.Context.Response.Headers[key] = value;
					continue;
				}

				if (string.Compare(key, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Context.Response.ContentLength64 = long.Parse(value);
				else if (string.Compare(key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase) == 0)
					pc.Context.Response.ContentEncoding = Encoding.GetEncoding(value);
				else
					pc.Context.Response.Headers[key] = value;
			}
			pc.Response.Headers["HTTP_VIA"] = this._HeaderValue;
			var input = pc.Response.GetResponseStream();
			var output = pc.Context.Response.OutputStream;
			var bs = new byte[BitUtil.K];
			while (true)
			{
				var i = input.Read(bs, 0, BitUtil.K);
				if (i == 0)
					break;

				if (pc.Output != null)
					pc.Output.Write(bs, 0, i);
				output.Write(bs, 0, i);
			}
			pc.Context.Response.Close();
		}

		protected virtual void EndGetResponse(ProxyContext pc, WebException x)//x
		{
			this._LastError = x;
			if (x.Status != WebExceptionStatus.Success && x.Status != WebExceptionStatus.ProtocolError)
			{
				//pc.Context.Response.Abort();
				pc.Context.Response.StatusCode = HttpStatus.Value.BAD_REQUEST;
				pc.Context.Response.StatusDescription = HttpStatus.Message.BAD_REQUEST;
				//pc.Context.Response.OutputStream.Write(
				pc.Context.Response.Close();
				return;
			}

			pc.Response = x.Response as HttpWebResponse;
			if (pc.Response == null)
			{
				//pc.Context.Response.Abort();
				pc.Context.Response.StatusCode = HttpStatus.Value.BAD_REQUEST;
				pc.Context.Response.StatusDescription = HttpStatus.Message.BAD_REQUEST;
				pc.Context.Response.Close();
				return;
			}

			pc.Context.Response.StatusCode = (int)pc.Response.StatusCode;
			pc.Context.Response.StatusDescription = pc.Response.StatusDescription;
			//pc.Context.Response.ContentType = pc.Response.ContentType;
			//pc.Context.Response.Headers.Add(pc.Response.Headers);
			foreach (string key in pc.Response.Headers.Keys)
			{
				var value = pc.Response.Headers[key];
				if (string.Compare(key, HttpHeaderKey.TRANSFER_ENCODING, StringComparison.OrdinalIgnoreCase) == 0)
				{
					if (string.Compare(value, HttpHeaderKey.Value.Chunked, StringComparison.OrdinalIgnoreCase) == 0)
						pc.Context.Response.SendChunked = true;
					else
						pc.Context.Response.Headers[key] = value;
					continue;
				}

				if (string.Compare(key, HttpHeaderKey.CONTENT_LENGTH, StringComparison.OrdinalIgnoreCase) == 0)
					pc.Context.Response.ContentLength64 = long.Parse(value);
				else if (string.Compare(key, HttpHeaderKey.TRANSFER_ENCODING, StringComparison.OrdinalIgnoreCase) == 0)
					pc.Context.Response.ContentEncoding = Encoding.GetEncoding(value);
				else
					pc.Context.Response.Headers[key] = value;
			}
			pc.Response.Headers[HttpHeaderKey.HTTP_VIA] = this._HeaderValue;
			var input = pc.Response.GetResponseStream();
			var output = pc.Context.Response.OutputStream;
			var bs = new byte[BitUtil.K];
			while (true)
			{
				var i = input.Read(bs, 0, BitUtil.K);
				if (i == 0)
					break;

				output.Write(bs, 0, i);
			}
			pc.Context.Response.Close();
		}

		protected virtual void EndGetResponse(ProxyContext pc, HttpListenerException x)//-
		{
			//try
			//{
			//    pc.Context.Response.Close();
			//}
			//catch { }
			return;
		}

		protected virtual void EndGetResponse(ProxyContext pc, Exception x)//-
		{
			pc.Context.Response.StatusCode = HttpStatus.Value.INTERNAL_SERVER_ERROR;
			pc.Context.Response.StatusDescription = HttpStatus.Message.INTERNAL_SERVER_ERROR;
			pc.Context.Response.Close();
		}

		protected virtual void Dispose(bool disposing)
		{
			this.Stop();
		}

		public void Dispose()
		{
			this.Dispose(false);
		}
	}
}
